<?php defined('SYSPATH') OR die('No direct access allowed.');
/**
 * File processor.
 *
 * @package    Fabryc
 * @author     J.B. Early
 * @copyright  (c) 2009 J.B. Early
 */
class Processor_Core
{
	public static function from_file($path)
	{
		$p = new Processor;
		$p->load_file($path);
		return $p;
	}

	public static function from_text($text)
	{
		$p = new Processor;
		$p->load_text($text);
		return $p;
	}

	private $path = '', $lines = array();
	public $description = '', $keywords = array(), $text='', $errors = array();

	public function __construct()
	{
	}

	public function load_text($t)
	{
		$this->lines = array();
		// first, join lines
		$t = $this->preEscape($t);
		// then, all the rest
		$t = $this->escapeText($t);
		// block comments out
		$t = $this->removeBlockComments($t);
		// line-by-line
		foreach (explode("\n",$t) as $line) {
			$this->lines[] = rtrim($line);
		}
	}

	public function load_file($path)
	{
		$this->path = $path;
		$this->load_text(@file_get_contents($path));
	}

	public function lines()
	{
		return $this->lines;
	}

	// escaping

	private $hashMap = array();

	private function hashChar($c) {
		@$hash = $this->hashMap[$c];
		if (!$hash) $this->hashMap[$c] = ($hash = "\001".ord($c)."\002");
		return $hash;
	}

	private function escape_callback($m) {
		if ("\n"==($c=$m[1])) return '';	// line continues
		return $this->hashChar($c);
	}

	// escape duble slashes and join lines
	private function preEscape($text) {
		return preg_replace_callback(
			'{\\\\([\\\\\n])}',
		array($this,'escape_callback'),
		$text);
	}

	private function escapeText($text) {
		return preg_replace_callback(
			'{\\\\(.)}s',
		array($this,'escape_callback'),
		$text);
	}

	public function unescapeText($text) {			// undo hashes
		return str_replace(
		array_values($this->hashMap),
		array_keys($this->hashMap),
		$text);
	}

	// comments

	private function removeBlockComments($text) {
		// block comments /## ... ##/
		$text = preg_replace(
			'/(\/\#\#.*?\#\#\/)/s',
			'',$text);
		$text = preg_replace(
			'/(\/\#\#.*$)/s',
			'',$text);
		return $text;
	}

	public function process()
	{
		// includes
		$lines = $this->lines();
		$allPaths = array($this->path=>true);
		$lines = $this->preIncludes($lines,array($this->path),$allPaths);

		$text = implode("\n",$lines);
		$text = $this->defineMacros($text);
		$text = $this->expandMacros($text);

		$this->text = $text;
	}

	// preprocess includes
	private function preIncludes($lines,$paths,&$allPaths) {
		$res = array();
		foreach ($lines as $line) {
			if (($incPath=$this->getIncludePath($line,$once))) {
				// include statement
				if (util::beginsWith('/',$incPath)) {
					// absolute include
					$incPath = DOCROOT.$incPath;
				} else {
					// relative include
					$incPath = dirname($this->path).'/'.$incPath;
				}

				if( ! ($incPath = realpath($incPath))) continue;

				if ($once && @$allPaths[$incPath])
				continue;
				if (false!==array_search($incPath,$paths))
				continue;			// already included in this branch

				$allPaths[$incPath] = true;
				$p = new Processor;
				$p->load_file($incPath);
				$res = array_merge($res,
				$this->preIncludes($p->lines(),
				array_merge(array($incPath),$paths),$allPaths));
				continue;				// included
			}

			$this->directives($line);

			// remove line comments
			$line = rtrim(preg_replace('/(\#\#.*$)/','',$line,-1,$count));
			if ($line or (0==$count)) $res[] = $line;
		}
		return $res;
	}

	private function getIncludePath($line,&$once) {
		if ( ! util::matchTok('##',$line))
		return false;
		if (util::matchTok('include_once',$line))
		$once = true;
		else if (util::matchTok('include',$line))
		$once = false;
		else
		return false;
		return util::getTok($line);
	}

	private function directives($line)
	{
		// other directives
		if (util::matchTok('##',$line)) {
			if (util::matchTok('description',$line)) {
				$this->description .= $line.' ';
			}
			elseif (util::matchTok('keywords',$line)) {
				$this->keywords = array_merge($this->keywords,explode(',',$line));
			}
		}
	}

	//---------------------------------------------------------------------
	// macros

	private $macroDefs;	// name => value

	private function define_macros_callback($m) {
		$this->macroDefs[$m[1]] = $m[2];
	}

	private function defineMacros($text) {
		// {$name=value$} name: [a-zA-Z_]\w*
		return preg_replace_callback(
			'/{\$\s*([a-zA-Z_]\w*)\s*=(.*?)\$}/ms',
		array($this,'define_macros_callback'),
		$text);
	}

	private $macroExpanded;					// controls looping

	private function explode_args($args,$atLeast) {
		return array_pad(explode('|',$args),$atLeast,'');
	}

	private function conditionals_callback($m) {
		$this->macroExpanded = true;

		$op = $m[1];
		list($a,$b,$c) = $this->explode_args($m[2],3);

		switch($op) {
			case '?': // $?[cond|T|F]
				return $a ? $b : $c;
			case '!': // $![cond|F|T]
				return $a ? $c : $b;
			case '=': // $=[a|b] ( a ? a : b)
				return $a ? $a : $b;
		}
	}

	private function expand_builtin_macros($name,$args) {
		switch($name) {
			case 'HD': // first character
				return substr($args[0],0,1);
			case 'TL': // the rest
				return substr($args[0],1);
			case 'UNESC': // unescape
				return $this->unescapeText($args[0]);
			case 'DOMAIN': // unescape
				return Kohana::config('core.site_domain',TRUE);
		}
	}

	private $undefinedMacros;

	private function expand_macro_callback($m) {
		$this->macroExpanded = true;

		$m    = array_pad($m,3,'');
		$name = $m[1];
		$args = $this->explode_args($m[2],10);

		$res = $this->expand_builtin_macros($name,$args);
		if($res) return $res;

		$res = @$this->macroDefs[$name];
		if(!$res) {
			$this->undefinedMacros[] = $name;
			return '';
		}

		// arguments @0 .. @9
		$nexti = 0;
		foreach ($args as $i=>$a) {
			$res = str_replace("@$i",$a,$res);
			$nexti = $i+1;
		}

		while ($nexti<=9) {
			$res = str_replace("@$nexti",'',$res);
			++$nexti;
		}

		return $res;
	}

	private $nestLimit	= 16;

	private function expandMacros($text) {		// macros and conditionals
		$nestLimit = $this->nestLimit;
		$this->undefinedMacros = array();

		do {
			$loop = false;

			// add brackets to inner macro symbols
			$text = preg_replace(
				'/\$([a-zA-Z_]\w*)\]/m',
				'\$$1[]]',
			$text);

			// now all inner $macros have brackets [...]
			do {
				$this->macroExpanded = false;
				// built-in conditionals
				$text = preg_replace_callback(
					'/\$([\?\!\=])\[([^\[]*?)\]/m',
				array($this,'conditionals_callback'),
				$text);
				// user-defined macros
				$text = preg_replace_callback(
					'{\$([a-zA-Z_]\w*)\[([^\[]*?)\]}m',
				array($this,'expand_macro_callback'),
				$text);

				if ($this->macroExpanded) $loop = true;	// do again

			} while ($this->macroExpanded && (--$nestLimit>0));

			// no more brackets, try once without
			$text = preg_replace_callback(
				'/\$([a-zA-Z_]\w*)/m',
			array($this,'expand_macro_callback'),
			$text);

			if ($this->macroExpanded) $loop = true;

		} while($loop && (--$nestLimit>0));

		if (!empty($this->undefinedMacros))
		$this->error('undefined macros: '.implode(', ',$this->undefinedMacros));

		return $text;
	}

	private function error($msg)
	{
		$this->errors[] = $msg;
	}

	public static function typography($text) {

		static $htmlChars;
		if (!isset($htmlChars)) {
			$htmlChars = array (

			//			'&' 	=> '&amp;',
			//			'(( ))'	=> '&nbsp;',
			//			'<-->'	=> '&harr;',
			//			'<--'	=> '&larr;',
			//			'-->'	=> '&rarr;',
			//			'<==>'	=> '&hArr;',
			//			'<=='	=> '&lArr;',
			//			'==>'	=> '&rArr;',
			//			'---'	=> '&mdash;',
			//			'--'	=> '&ndash;',
			'...'	=> '&hellip;',
			//			'((r))'	=> '&reg;',
			//			'((c))'	=> '&copy;',
			//			'((tm))'=> '&trade;',
			//			'<' 	=> '&lt;',
			//			'>' 	=> '&gt;',

			);
		}

		return str_ireplace(
		array_keys($htmlChars),
		array_values($htmlChars),
		$text);
	}
}

// eof

/*<?php // Copyright 2009 J.B. Early - jbnotes.info

require_once dirname(__FILE__).'/../common/util.php';
require_once dirname(__FILE__).'/../common/stringy.php';

/*
ST: a token, or "....", or '....'

1st phase: preprocessor
-------------------------------------------
include files
:: [ ]##[ ]include file.ma:ST
TODO include raw

in index.ma only
:: [ ]##[ ]menu file.ma:ST [tag:ST]

later
a single file scope:
##escapechar \
##macrochar $
##preprocesschar #
till the end scope:
##metachar @

a) read menu
b) include files
escape characters \
strip comments: ## line comment
/## block comment ##/
block comments even comment out (special) line comments

2nd phase: macros
-------------------------------------------
(already done: includes, escapes)

a) define macros
{$name=value$}
b) expand macros
conditionals:
$?[cond|T|F]
$![cond|F|T]
$=[a|b]  (a or b)

PCRE modifiers
s: dotall, . matches \n
m: multiline, $ before \n
* /

class MamaFile {

protected 	$path,		// converted to real path
$rootDir,	// the root directory for includes
$incDirs;	// allowed include directories
// or empty = all allowed

public function __construct($path,$rootDir,$incDirs=array()) {
if (!beginsWith('/',$path)) {
$this->error('path must be absolute');
return;
}

$this->rootDir	= realpath($rootDir);
$this->path		= realpath($this->rootDir.$path);
$this->incDirs	= array();
foreach (asArray($incDirs) as $d) {
$this->incDirs[] = realpath($d);
}
}

private function reset() {
$this->hashMap		= array();
$this->macroDefs	= array();
}

private $content = '';

public function load() {
$this->reset();

$lines = $this->loadFromPath($this->path);
$allPaths = array($this->path=>true);
$lines = $this->preInclude($lines,
array($this->path),$allPaths);

$text = implode("\n",$lines);
$text = $this->defineMacros($text);
$text = $this->expandMacros($text);

$this->content = $text;
}

public $raiseHell = true;
private function error($msg,$value='') {
if (!$this->silentErrors) raise($msg);
return $value;
}

private function isValid($path) {
return is_file($path) && is_readable($path);
}

private function isAllowed($path) {
if (!$this->isValid($path))
return false;

if (empty($this->incDirs))
return true;

foreach ($this->incDirs as $dir) {
if (beginsWith($dir,$path)) return true;
}

return false;
}

private function loadFromPath($path) {
if (!$this->isAllowed($path))
return $this->error("invalid path",array());

$t = @file_get_contents($path);
// first, join lines
$t = $this->preEscape($t);
// then, all the rest
$t = $this->escapeText($t);
// block comments out
$t = $this->removeBlockComments($t);
// line-by-line
foreach (explode("\n",$t) as $line) {
$lines[] = rtrim($line);
}

return $lines;
}

public function menuFromPath($path) {
$menu = array();
$toks = array('##','menu');
foreach ($this->loadFromPath($path) as $line) {
if (2==matchTok($toks,$line)) {
if (!($file = getTok($line))) {
$this->error('empty menu directive');
continue;
}

if (!($tag = getTok($line))) {
$p = pathinfo($file);
$tag = $p['filename'];
}

$menu[] = array($file,$tag);
}
}
return $menu;
}

// preprocess includes
private function preInclude($lines,$paths,&$allPaths) {
$res = array();
foreach ($lines as $line) {
if (($incPath=$this->getIncludePath($line,$once))) {
// include statement
if (beginsWith('/',$incPath)) {	// absolute include
$incPath = $this->rootDir.$incPath;
} else {				// relative include
$incPath = dirname($paths[0]).'/'.$incPath;
}
$incPath = realpath($incPath);

if ($once && $allPaths[$incPath])
continue;
if (false!==array_search($incPath,$paths))
continue;			// already included in this branch

$allPaths[$incPath] = true;
$incLines = $this->loadFromPath($incPath);
$res = array_merge($res,$this->preInclude($incLines,
array_merge(array($incPath),$paths),$allPaths));
continue;				// included
}
// remove line comments
$line = preg_replace('/(\#\#.*$)/','',$line);

$res[] = $line;
}
return $res;
}

private function getIncludePath($line,&$once) {
if (!matchTok('##',$line))
return false;
if (matchTok('include_once',$line))
$once = true;
else if (matchTok('include',$line))
$once = false;
else
return false;
return getTok($line);
}

//---------------------------------------------------------------------

private private function hashMs($text) {		// hash markup characters
static $ms;
if (!isset($ms)) {
$cs = str_split('&;<>*   /_^~-:=`{}[]"\'');	// chars that are used in markups
foreach ($cs as $c) $ms[$c] = $this->hashChar($c);
}

return str_replace(array_keys($ms),array_values($ms),$text);
}

private private function hashCode($text) {
return $this->hashMs(
str_replace(' ','&nbsp;',htmlspecialchars($text))
);
}

//---------------------------------------------------------------------
//---------------------------------------------------------------------
// macros

private $macroDefs;	// name => value

private function define_macros_callback($m) {
$this->macroDefs[$m[1]] = $m[2];
}

private function defineMacros($text) {
// {$name=value$} name: [a-zA-Z_]\w*
return preg_replace_callback(
'/{\$\s*([a-zA-Z_]\w*)\s*=(.*?)\$}/ms',
array($this,'define_macros_callback'),
$text);
}

private $macroExpanded;					// controls looping

private function explode_args($args,$atLeast) {
return array_pad(explode('|',$args),$atLeast,'');
}

private function conditionals_callback($m) {
$this->macroExpanded = true;

$op = $m[1];
list($a,$b,$c) = $this->explode_args($m[2],3);

switch($op) {
case '?': // $?[cond|T|F]
return $a ? $b : $c;
case '!': // $![cond|F|T]
return $a ? $c : $b;
case '=': // $=[a|b] ( a ? a : b)
return $a ? $a : $b;
}
}

private function expand_builtin_macros($name,$args) {
switch($name) {
case 'HD': // first character
return substr($args[0],0,1);
case 'TL': // the rest
return substr($args[0],1);
case 'UNESC': // unescape
return $this->unescapeText($args[0]);
}
}

private $undefinedMacros;

private function expand_macro_callback($m) {
$this->macroExpanded = true;

$m    = array_pad($m,3,'');
$name = $m[1];
$args = $this->explode_args($m[2],10);

$res = $this->expand_builtin_macros($name,$args);
if($res) return $res;

$res = $this->macroDefs[$name];
if(!$res) {
$this->undefinedMacros[] = $name;
return '';
}

// arguments @0 .. @9
$nexti = 0;
foreach ($args as $i=>$a) {
$res = str_replace("@$i",$a,$res);
$nexti = $i+1;
}

while ($nexti<=9) {
$res = str_replace("@$nexti",'',$res);
++$nexti;
}

return $res;
}

private $nestLimit	= 16;

private function expandMacros($text) {		// macros and conditionals
$nestLimit = $this->nestLimit;
$this->undefinedMacros = array();

do {
$loop = false;

// add brackets to inner macro symbols
$text = preg_replace(
'/\$([a-zA-Z_]\w*)\]/m',
'\$$1[]]',
$text);

// now all inner $macros have brackets [...]
do {
$this->macroExpanded = false;
// built-in conditionals
$text = preg_replace_callback(
'/\$([\?\!\=])\[([^\[]*?)\]/m',
array($this,'conditionals_callback'),
$text);
// user-defined macros
$text = preg_replace_callback(
'{\$([a-zA-Z_]\w*)\[([^\[]*?)\]}m',
array($this,'expand_macro_callback'),
$text);

if ($this->macroExpanded) $loop = true;	// do again

} while ($this->macroExpanded && (--$nestLimit>0));

// no more brackets, try once without
$text = preg_replace_callback(
'/\$([a-zA-Z_]\w*)/m',
array($this,'expand_macro_callback'),
$text);

if ($this->macroExpanded) $loop = true;

} while($loop && (--$nestLimit>0));

if (!empty($this->undefinedMacros))
$this->error('undefined macros: '.implode(', ',$this->undefinedMacros));

return $text;
}

//---------------------------------------------------------------------
// output

public function asHtml() {
$text = $this->content;

// anchors
$text = preg_replace_callback(          // :text:[link]
'/:([^\s].*?)(?<!\s):\[(.*?)\]/',array($this,'a_callback'),$text);
$text = preg_replace_callback(          // #[name]
'/\#\[(.*?)\]/',array($this,'a_name_callback'),$text);

$text = preg_replace_callback(          // [*img*]:[link]
'/\[\*(.*?)\*\]:\[(.*?)\]/',array($this,'img_callback'),$text);

// images
$text = preg_replace_callback(          // [*img*]
'/\[\*(.*?)\*\]/',array($this,'img_callback'),$text);

// html characters
$text = preg_replace_callback('/\{\&(\w+)\}/',array($this,'html_callback'),$text);

// hr
$text = preg_replace('/^\s*---+$/m','!<hr/>!',$text);

$text = $this->processMarkup($text);

$text = $this->typography($text);

// quotes
$text = preg_replace(
array('/"([^\s][^"]*?[^\s])"/s', '/\'(\w[^\']*?\w)\'/s'),
array('&ldquo;$1&rdquo;','&lsquo;$1&rsquo;'),$text);

$text = $this->unescapeText($text);

return $text;
}

private function a_callback($m) {
$href = trim($m[2]);
if (!$href) $href = '#';
return $this->hashMs("<a href='$href'>").$m[1].$this->hashMs('</a>');
}

private function a_name_callback($m) {
return $this->hashMs("<a name='$m[1]'></a>");
}

private function img_callback($m) {
list($src,$alt) = $this->explode_args($m[1],2);
$url = $m[2];
if ($url)
return $this->hashMs("<a href='$url'><img src='$src' alt='$alt'/></a>");
else
return $this->hashMs("<img src='$src' alt='$alt'/>");
}

private function html_callback($m) {
return $this->hashMs("&$m[1];");
}

//--- typography -----

function typography($text) {
static $htmlChars;
if (!isset($htmlChars)) {
$htmlChars = array (

'&' 	=> '&amp;',
'(( ))'	=> '&nbsp;',
'<-->'	=> '&harr;',
'<--'	=> '&larr;',
'-->'	=> '&rarr;',
'<==>'	=> '&hArr;',
'<=='	=> '&lArr;',
'==>'	=> '&rArr;',
'---'	=> '&mdash;',
'--'	=> '&ndash;',
'...'	=> '&hellip;',
'((r))'	=> '&reg;',
'((c))'	=> '&copy;',
'((tm))'=> '&trade;',
'<' 	=> '&lt;',
'>' 	=> '&gt;',

);
}

return str_ireplace(
array_keys($htmlChars),
array_values($htmlChars),
$text);
}

//---------------------------------------------------------------------
// markup

private function processMarkup($text) {

// split the text by <html tags>
// no markup will be processed inside of <...>

$lines = array(); $line = '';

$parts = preg_split('/(\!<.*?>\!)/s',$text,-1,PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY);

foreach ($parts as $part) {
if (('!<'==substr($part,0,2)) && ('>!'==substr($part,-2))) {
// html tag
$line .= $this->hashMs(substr($part,1,strlen($part)-2));
} else {
$e = explode("\n",$part);

if($piece = array_shift($e))
$line .= $this->processPart($piece);

while (null!==($piece=array_shift($e))) {
$lines[] = $line;
$line = $this->processPart($piece);
}
}
}

$lines[] = $line;

// now for the real part
// line-by-line
$text = '';
while (!empty($lines)) {

if ($t = $this->qHeader($lines)) {
$text .= $this->emitPara() . $t;
continue;
};

if ($t = $this->qPre($lines)) {
$text .= $this->emitPara() . $t;
continue;
}

if ($t = $this->qList($lines)) {
$text .= $this->emitPara() . $t;
continue;
}

if ($t = $this->qTable($lines)) {
$text .= $this->emitPara() . $t;
continue;
}

// para
if ($t = $this->getLine($lines)) {
preg_match('/^\s*(.*)$/',$t,$matches);
$this->addToPara($matches[1]);
} else {
// empty
$text .= $this->emitPara();
}
}

$text .= $this->emitPara();

// now the rest
$text = $this->doCurly($text);
$text = $this->doFootnotes($text);

return $text;
}

private function processPart($text) {

static $ms = '\`\*\/\_\~\^\-';	// markup chars

if (!preg_match("/[$ms]/",$text))
return $text;				// shortcut

// else do pair markup
static $subst;
if(!isset($subst)) {
$subst = array(	// doing it inside out except for `...`
array('\`',		'\`',	'code',	''),
array('\*',		'\*',	'b',	$ms),
array('\/',		'\/',	'i',	$ms),
array('\_',		'\_',	'u',	$ms),
array('\~',		'\~',	'sub',	$ms),
array('\^',		'\^',	'sup',	$ms),
array('\-\-',	'\-\-',	'del',	$ms),
);
}

do {
$count = 0;
foreach ($subst as $s) {
list($mb,$me,$tag,$ex) = $s;
$text = $this->pairMarkup($text,$mb,$me,$tag,$count,$ex);
}
} while($count>0);

return $text;
}

/* $mb:	 markup begin
* $me:  markup end
* $tag: start tag
*
* $count:	will be incremented by how many were replaced
* $except:	characters that must not be inside the match
* /

private $c_pair,$c_count;

private function pair_callback($m) {
$this->c_count++;
list($st,$et,$tag) = $this->c_pair;
$t = $st.$m[1].$et;
if ('code'==$tag/*hack* /)
$t = $this->hashCode($t);
else
$t = $this->hashMs($t);
return $t;
}

private function pairMarkup($text,$mb,$me,$tag,&$count,$except='') {

$match = $except ? "[^$except]" : '.';

$this->c_pair = array($this->startTag($tag), $this->endTag($tag), $tag);
$this->c_count = 0;

$text = preg_replace_callback(
"/(?<!$mb)$mb(?!\\s)($match+?)(?<!\\s)$me(?!$me)/s",
array($this,'pair_callback'),
$text);

$count += $this->c_count;

return $text;
}

// paragraphs

private $para = '';

private function addToPara($t) {
if ($this->para) $t = "\n$t";
$this->para .= $t;
}

private function emitPara() {
if ($p = $this->para) {
$this->para = '';
return $this->inTag('div class="para"','div',"\n$p");
}
}

private function curly_callback($m) {
$this->curlyDone = true;
list($all,$class,$mod,$text) = $m;
if ($class) $class = " class='$class'";

switch ($mod) {
case ':':
return $this->inTag("span$class",'span',$text);
case '=':
return $this->inTag("div$class",'div',$text);
default:
return $this->hashMs($all);
}
}

private function doCurly($text) {
$nestLimit = $this->nestLimit;

do {
$this->curlyDone = false;
// inside out
$text = preg_replace_callback(
'{\{(\w*)(.)([^\{]*?)\}}m',
array($this,'curly_callback'),
$text);

if ($this->curlyDone) $loop = true;
} while ($this->curlyDone && (--$nestLimit>0));

return $text;
}

private function footnote_callback($m)
{
$this->footnotes[] = $m[1];
$fn = sizeof($this->footnotes);
return $this->inTag("a name='footnote_link_$fn'",'a','').
$this->inTag('sup','sup',
$this->inTag("a class='footnote' href='#footnote_$fn'",'a',"($fn)"));
}

private $footnotes;
private function doFootnotes($text)
{
$this->footnotes = array();

$text = preg_replace_callback(
'{\(\(\((.*?)\)\)\)}ms',
array($this,'footnote_callback'),
$text);

if(!empty($this->footnotes))
{
$text .= "\n".$this->singleTag('hr')."\n".
$this->inTag('small','small',$this->inTag('b','b','Footnotes:')).
$this->singleTag('br')."\n";

foreach($this->footnotes as $i=>$f)
{
$fn = $i+1;
$text .=
$this->inTag("a name='footnote_$fn'",'a','').
$this->inTag('sup','sup',$this->inTag("a class='footnote' href='#footnote_link_$fn'",'a',"($fn) $f ")).
$this->singleTag('br')."\n";
}
}
return $text;
}

private function curLine(&$lines) {
return empty($lines) ? '' : $lines[0];
}

private function getLine(&$lines) {
if (empty($lines)) return '';
return array_shift($lines);
}

private $hlevel	= 7;	// headers
private function qHeader(&$lines) {
if (preg_match('/^\s*(===+)(.+?)(=*)$/',$this->curLine($lines),$matches)) {
$level = $matches[1]; $h = $matches[2];
$this->getLine($lines);
$level = $this->hlevel - strlen($matches[1]);
if ($level<1) $level = 1;
return $this->inTag("h$level","h$level",$h)."\n";
}
}

private function qPre(&$lines) {
if (preg_match('/^\s*{(\w*)\+{2,}(.*)$/',$this->curLine($lines),$matches)) {
if($class = $matches[1]) $class = " class='$class'";
$res = $this->startTag("pre$class").$this->hashCode($matches[2])."\n";
for(;;) {
$this->getLine($lines);
if (empty($lines) ||
(($line=$this->curLine($lines)) && preg_match('/^(.*?)\+{2,}}(.*)$/',$line,$m)) ) {
$this->getLine($lines);
return $res.$this->hashCode($m[1]).$this->endTag('pre')."\n$m[2]";
}
$res .= $this->hashCode($line)."\n";
}
}
}

private function qList(&$lines) {

$m = '\*'; $tag = 'ul';
if (!preg_match('/^(\s*)'.$m.'\s+(.*)$/',$this->curLine($lines),$matches)) {
$m = '[0-9]+\.'; $tag = 'ol';
if (!preg_match('/^(\s*)'.$m.'\s+(.*)$/',$this->curLine($lines),$matches))
return;
}

$indent = strlen($matches[1]);

$text = $this->startTag($tag);
$text .= $this->startTag('li').$matches[2];
$this->getLine($lines);

$inli = true;
for(;;) {
if (!$line=$this->curLine($lines)) break;
if (preg_match('/^(\s*)'.$m.'\s+(.*)$/',$line,$matches)) {
$indent1 = strlen($matches[1]);
if ($indent==$indent1) {
if ($inli) $text .= $this->endTag('li')."\n";
$text .= $this->startTag('li').$matches[2];
$this->getLine($lines);
$inli = true;
continue;
}
if ($indent<$indent1) { // nest
$text .= $this->qList($lines,$ordered);
$text .= $this->endTag('li');
$inli = false;
continue;
}
// $indent>$indent1
break;
}
if ($inli && preg_match('/^\s+(.*)$/',$line,$matches)) {
// continuation
$text .= "\n$matches[1]";
$this->getLine($lines);
continue;
}
break;
}

if ($inli)
$text .= $this->endTag('li')."\n";

$text .= $this->endTag($tag)."\n";

return $text;
}

private function qTable(&$lines) {

while (preg_match('/^\|(.*)\|$/',$this->curLine($lines),$matches)) {
$this->getLine($lines);
$row = '';
foreach (explode('|',$matches[1]) as $col) {
switch ($col[0]) {
case '=':
$tag = 'th';
$col = substr($col,1);
break;
default:
$tag = 'td';
}

switch ($col[0]) {
case '2':
$extra = ' colspan="2"';
$col = substr($col,1);
break;
case '3':
$extra = ' colspan="3"';
$col = substr($col,1);
break;
default:
$extra = '';
}
$row .= $this->inTag("$tag$extra",$tag,$col);
}
$text .= $this->inTag('tr','tr',$row);
}

if ($text) $text = $this->inTag('table summary=""','table',$text);
return $text;
}

//---------------------------------------------------------------------
// html

private function startTag($t) {
static $tags;
if (!($tag=$tags[$t]))
$tags[$t] = ($tag=$this->hashMs("<$t>"));
return $tag;
}

private function endTag($t) {
static $tags;
if (!($tag=$tags[$t]))
$tags[$t] = ($tag=$this->hashMs("</$t>"));
return $tag;
}

private function singleTag($t) {
static $tags;
if (!($tag=$tags[$t]))
$tags[$t] = ($tag=$this->hashMs("<$t/>"));
return $tag;
}

private function inTag($start,$end,$text) {
return $this->startTag($start).$text.$this->endTag($end);
}
}


/*
public $tabStop		= 4;
static function expandTabs($text,$tabStop) {	// O'Reilly cookbook
while (strstr($text,"\t")) {
$text = preg_replace(
'/^([^\t]*)(\t+)/e',
"'\\1'.str_repeat(' ',strlen('\\2')*$tabStop-strlen('\\1')%$tabStop)",
$text);
}
return $text;
}

function cleanupLines($text) {	// untab and trim right
$lines = explode("\n",$text);
foreach ($lines as $i => $line) {
$lines[$i] = $this->expandTabs(preg_replace('/\s+$/','',$line),$this->tabStop);
}
return implode("\n",$lines);
}
*/